The Tree Widget
Much of the information that computers deal with is hierarchical, or
can be viewed as a hierarchy. A UNIX file system is one of the most
common examples. The Tk widget hierarchy is another. One of the best
ways to display an overview of this kind of data is in the form of a
tree.
This chapter describes a widget for displaying trees in Tk. The tree
widget really has two interfaces. The first is to the basic Tk widget,
which is implemented in C++. This is the most flexible and is the one
we will look at first. The second interface is implemented as an [incr
Tcl] class built on top of the tree widget. The [incr Tcl] class
offers a simpler, but less flexible interface that displays trees
where each node is made up of a label and an optional bitmap. We will
come to that later on.
The tree widget differs from most other Tk widgets in that it doesn't
display its own window and does not need to be packed in the
window hierarchy. What the tree widget does is manage items that you
create in a Tk canvas. You create the tree as a child of a
canvas widget and then tell it which canvas items make up each tree
node and line. The tree widget calculates the layout and repositions
the canvas items to make the tree. Using canvas items for the tree
nodes and lines has some great advantages. The tree nodes can be made
up of any combination of canvas items, including text, bitmaps, images
and other graphics. You design the lines connecting the tree nodes, so
you can make them in any color, style or thickness you like.
A Simple Tree
Before we get into any more detail, let's look at an example of a
simple tree application: a directory tree. The program below uses the
Unix ls(1) command to recursively build up a tree of directories. Its
only a simple demonstration, so there are no menus and no bindings
yet. (Note that all of the examples shown here can be found in
the demos directory of the tree widget extension.)
# create a canvas with horizontal and vertical scrollbars in the
# given frame with the given name
proc MakeCanvas {frame canvas} {
set vscroll [scrollbar $frame.vscroll -command "$canvas yview"]
set hscroll [scrollbar $frame.hscroll -orient horiz -command "$canvas xview"]
set canvas [canvas $canvas -xscroll "$hscroll set" -yscroll "$vscroll set"]
pack $vscroll -side right -fill y
pack $hscroll -side bottom -fill x
pack $canvas -fill both -expand 1
bind $canvas "$canvas scan mark %x %y"
bind $canvas "$canvas scan dragto %x %y"
return $canvas
}
# add the directories under $dir to the tree (recursively)
proc ListDirsRec {canvas tree dir} {
foreach i [exec ls $dir] {
if {[file isdir $dir/$i]} {
AddDir $canvas $tree $dir $dir/$i $i
ListDirsRec $canvas $tree $dir/$i
}
}
}
# add the given directory to the tree
#
# Args:
# canvas - tree's canvas
# tree - the tree
# parent - pathname of parent dir
# dir - pathname of new dir being added
# text - text for tree node label (last component of dir)
proc AddDir {canvas tree parent dir text} {
$canvas create text 0 0 -text $text -tags $dir
set line [$canvas create line 0 0 0 0 -tag "line"]
$tree addlink $parent $dir $line
}
# main
wm geometry . 400x275
set canvas [MakeCanvas . .c]
set tree [tree $canvas.t -layout vertical]
cd ..
set dir [pwd]
AddDir $canvas $tree {} $dir [file tail $dir]
ListDirsRec $canvas $tree $dir
The figure below shows the window produced by the above code when run
from the demos directory in the tree-4.0 distribution
(the rootnode directory is a dummy directory created
for test purposes).
Creating the Tree
The first thing you need to do to use the tree widget is to create a
canvas widget. The MakeCanvas procedure above creates a
canvas with two scrollbars and the usual Tk bindings, so you can
scroll by dragging the middle mouse button. As we will see later on,
the [incr Tcl] tree interface takes care of this part for us. The
tree widget itself is created toward the end of the example with the
following command:
set tree [tree $canvas.t -layout vertical]
The tree widget, like all Tk widgets, returns its path name, in this
case .c.t, when created. For clarity, I prefer to use
variable names such as $canvas or $tree to refer to
widgets rather than the path names. In this case the path name is
quite short, but that is not usually the case.
The tree widget must be created as a child of the canvas widget it
will manage (in fact the tree widget derives the name of the canvas
widget from its own path name). The default layout of the tree is
horizontal or left to right. In this example the layout was
set to vertical. You can change the layout at any time by
using the configure subcommand:
$tree configure -layout horizontal
This produces a tree as shown below.
Adding Nodes to the Tree
Tree nodes are made up of one or mode canvas items, such as text,
bitmap, image or other graphics. Canvas items in Tk are referred to
by their tags or Ids. Each canvas item has an id and an optional list
of tags, where a tag is any (non-integer) string. You can refer to a
group of canvas items by assigning them all a common tag. This
feature is used by the tree widget to refer to the tree nodes as well.
When the tree widget is created, a single, invisible root node is also
created. This node is given an empty tag and can be referred to
as "" or {}. To start adding nodes to the tree, you use
the tree subcommand addlink:
$canvas create text 0 0 -text $text -tags $dir
set line [$canvas create line 0 0 0 0 -tag "line"]
$tree addlink $parent $dir $line
The addlink subcommand takes three arguments, which are the
canvas tags or ids of the parent node, the node you
are adding to the tree and the line connecting them. If the
new node should be the root of the tree, then parent is
specified as the empty tag {}. In the example above, the
tree node consists only of a canvas text item displaying the last
component of the directory name $text. We use the full path
name $dir as the canvas tag, since it uniquely describes the
node. We could also have used some other unique value, such as the the
directory's i-node. The label is created at the arbitrary position (0,
0). The tree widget will move it to its final position, so we can
place it anywhere we like. Likewise the line connecting the parent
and child nodes is created as a canvas line with length 0 at position
(0, 0), since the tree widget will position the line as needed. The
advantage of creating the line yourself rather than having the tree
widget do it for you is that you can specify the color, thickness and
other properties of the line when you create it. The line does not
need to (and should not) have the same tag as the node. It is enough
to specify the line's id, as returned from the canvas create
command. In fact, in the example above, we could also have specified
the canvas id for the node's label rather than using the
$dir tag, but that would only work as long as the node is
made up of a single canvas item.
More Complex Nodes
To illustrate the point, let's modify the example above to
add a bitmap image to each node. To do that we modify the
AddDir procedure from the example above and add
a new procedure LayoutNode to arrange the items
in the node:
# add the given directory to the tree
#
# Args:
# canvas - tree's canvas
# tree - the tree
# parent - pathname of parent dir
# dir - pathname of new dir being added
# text - text for tree node label (last component of dir)
proc AddDir {canvas tree parent dir text} {
set bitmap @bitmaps/dir.xbm
$canvas create text 0 0 -text $text -tags [list $dir text $dir:text]
$canvas create bitmap 0 0 -bitmap $bitmap -tags [list $dir bitmap $dir:bitmap]
set line [$canvas create line 0 0 0 0 -tag "line"]
LayoutNode $canvas $tree $dir
$tree addlink $parent $dir $line
}
# layout the components of the given node depending on whether
# the tree is vertical or horizontal
proc LayoutNode {canvas tree dir} {
set text $dir:text
set bitmap $dir:bitmap
if {[$tree cget -layout] == "horizontal"} {
scan [$canvas bbox $text] "%d %d %d %d" x1 y1 x2 y2
$canvas itemconfig $bitmap -anchor se
$canvas coords $bitmap $x1 $y2
} else {
scan [$canvas bbox $bitmap] "%d %d %d %d" x1 y1 x2 y2
$canvas itemconfig $text -anchor n
$canvas coords $text [expr "$x1+($x2-$x1)/2"] $y2
}
}
The figure below shows what the new tree looks like.
In the example above, each node's bitmap and label are initially
created at the position (0, 0) and then the LayoutNode
procedure is called to center the bitmap above the label for vertical
trees or to the left of the label for horizontal trees. All of the
components that make up a node are later moved as a unit to their
final position in the tree, so the relative positions of the items
stay the same. The LayoutNode procedure was written so that
it could be called again for each node if the tree's layout is
changed. For example, we could now toggle the tree's layout between
vertical and horizontal with the following procedure:
# Toggle the layout of the tree between vertical and horizontal
proc ToggleLayout {canvas tree} {
if {[$tree cget -layout] == "horizontal"} {
$tree config -layout vertical
} else {
$tree config -layout horizontal
}
# change the layout of the nodes so that the bitmap is on top for
# vertical trees and at left for horizontal trees
foreach i [$canvas find withtag text] {
set dir [lindex [$canvas gettags $i] 0]
LayoutNode $canvas $tree $dir
$tree nodeconfig $dir
}
}
What this procedure does is loop through all of the tree nodes and
change the layout of the items in each node depending on the layout of
the tree (horizontal or vertical). The
nodeconfig tree subcommand is then called for each node to
tell the tree widget to recalculate the node's size and position. The
figure below shows the same tree now with the layout set to
horizontal.
Using Canvas Tags to Reference Nodes
In the previous example the canvas items that make up a tree
node were each given a list of three tags:
$canvas create text 0 0 -text $text -tags [list $dir text $dir:text]
$canvas create bitmap 0 0 -bitmap $bitmap -tags [list $dir bitmap $dir:bitmap]
The first tag $dir is shared by all of the items that make up
a single node and is used to reference that node. the
ToggleLayout procedure depends on this tag being the first in
the list. The second tag identifies all of the node labels
(text) or all of the bitmaps (bitmap) in the
tree. The third tag is a combination of the first two and identifies
the label or bitmap of a particular tree node. This is used in the
LayoutNode procedure to change the position of the label and
bitmap of each node.
The command:
$canvas find withtag text
returns a list of all the canvas items having the tag
text. Since each of the tree nodes we created above also has
a label with this tag, the return value is a list of all of the labels
in the tree. Since we know that
the $dir tag (the directory path name for the node) is the
first in the list of tags, it can be retrieved with the command:
set dir [lindex [$canvas gettags $i] 0]
where $i is the id for the node's label or bitmap. We also use this
feature later on to get the directory name associated with a node
when the user clicks on it with the mouse.
Changing the Distance between Sibling Nodes
Depending on the layout of the tree nodes, you may want to add some
extra space between the nodes. For example, in the horizontal tree
shown above, the layout looks a bit crowded. You can change this by
specifying a border space around the individual nodes with the
-border: option to addlink
proc AddDir {canvas tree parent dir text} {
...
$tree addlink $parent $dir $line -border 2m
}
Although this option is set separately on each node, it usually looks
best when all of the nodes have the same border size, as shown below.
By default there is no border around a node:
Changing the Distance between Parent and Child Nodes
You can also change the distance between parent and child nodes.
Unlike the -border option above, which is applied to each
node, the -parentdistance option is set once for the entire
tree, either as an option when creating the tree or through the
configure widget subcommand. The default parentdistance
is 30 pixels, however you can use any of the standard Tk units, such as
c for centimeter as shown below:
proc AddDir {canvas tree parent dir text} {
...
$tree addlink $parent $dir $line -border 2m
}
...
set tree [tree $canvas.t -layout horizontal -parentdistance 3c]
...
The resulting tree is shown below:
Manipulating Trees
In the previous examples we have seen how to create and display a tree
in a Tk canvas. Most applications that display trees also need to
modify them dynamically and possibly allow the user to manipulate them
directly. In this section we will discuss ways to examine and modify
the tree structure and handle user input.
Selecting Tree Nodes
Let's extend the example directory tree application to allow the user
to select tree nodes with the mouse and perform operations on them.
Since the tree nodes are made up of canvas items, we use the canvas
widget's commands to handle the user input. First we add some bindings
so that clicking with mouse button 1 on a tree node highlights the label
or bitmap and prints out the pathname of the selected directory.
The bindings and the procedures that implement them are listed below.
# set the bindings for the tree canvas
proc SetBindings {canvas tree} {
# bind mouse button <1> to select the label or bitmap
$canvas bind text <1> "focus %W; SelectNode $canvas"
$canvas bind bitmap <1> "SelectBitmap $canvas"
}
# select the current node's label
proc SelectNode {canvas} {
$canvas select from current 0
$canvas select to current [string length [$canvas itemcget current -text]]
DeSelectBitmap $canvas
puts "selected label: [GetPath $canvas]"
}
# de-select all node labels
proc DeSelectNode {canvas} {
$canvas select clear
}
# highlight the node's bitmap
proc SelectBitmap {canvas} {
catch {focus {}}
set path [lindex [$canvas gettags current] 0]
DeSelectNode $canvas
DeSelectBitmap $canvas
$canvas itemconfig current -background [$canvas cget -selectbackground] \
-tags "[$canvas gettags current] selected"
puts "selected bitmap: [GetPath $canvas]"
}
# stop highlighting the node's bitmap
proc DeSelectBitmap {canvas} {
$canvas itemconfig selected -background [$canvas cget -background]
$canvas dtag selected
}
# return the pathname (dir) for the item currently selected (bitmap or text)
proc GetPath {canvas} {
set id [$canvas select item]
if {"$id" == ""} {
return [lindex [$canvas gettags selected] 0]
}
return [lindex [$canvas gettags $id] 0]
}
The code to select a node's label is fairly straight forward. You use
the canvas features to select the text item under the mouse, given by
the special canvas tag current. Since there is no predefined
way of selecting a bitmap, we simply set the bitmap's background color
to the same color used to display selected text and give it the tag
selected, which otherwise has no special meaning in this
context. The GetPath routine checks for either a selected
label or a selected bitmap and returns the first tag in the list of
tags for the item, which is in this case the directory path name.
On my machine, clicking on the node highlighted above produced the following
output:
selected label: /home/tcl/new/dist/tree-4.0/demos/rootnode
You could of course use other methods to select a node, such as drawing
a border around it or changing the bitmap or image used to display it.
Editing Trees Interactively
Suppose we want to allow the user to navigate the tree by
double-clicking on the tree nodes. We could define the following
behavior:
- A double-click on a node's bitmap either makes the node the new
root of the tree, or if the node is already the root node, then the
node's parent and siblings are added to the tree.
- A double-click on a node's label will add any subnodes
(subdirectories here) to the tree, or if the subnodes are already in
the tree, remove them.
To implement this behavior we add two new bindings that call some new
Tcl procedures. These procedures make use of a number of tree
subcommands that we haven't discussed yet, but I will explain them
shortly.
$canvas bind text "ToggleChildren $canvas $tree"
$canvas bind bitmap "ToggleParent $canvas $tree"
# If the current selection is a leaf, add its subnodes, otherwise
# remove them
proc ToggleChildren {canvas tree} {
set path [GetPath $canvas]
if [$tree isleaf $path] {
ListDirs $canvas $tree $path
$tree draw
} else {
$tree prune $path
}
}
# If the selected node is the root of the tree, add its parent and siblings
# to the tree, otherwise make the selected node the new root of the tree.
proc ToggleParent {canvas tree} {
global dirtree
set path [GetPath $canvas]
if [$tree isroot $path] {
set dir [file dirname $path]
if {$dir != $path} {
AddDir $canvas $tree "" $dir [dir_tail $dir]
set tail [file tail $path]
foreach i [exec ls $dir] {
if {[file isdirectory $dir/$i]} {
if {$i == $tail} {
$tree movelink $path $dir
} else {
AddDir $canvas $tree $dir $dir/$i "$i"
}
}
}
$tree draw
}
} else {
$tree root $path
}
}
# return the last component of the directory name
# (/ is a special case)
proc dir_tail {dir} {
if {$dir == "/"} {return $dir}
return [file tail $dir]
}
The ToggleChildren procedure adds child nodes to a node if
none are present or removes them otherwise. First, we use the
GetPath procedure defined earlier to get the tag for the
selected node, which is, in our case, the directory path name used to
uniquely identify the node.
To determine if the node has any subnodes, we use the isleaf
tree subcommand. Given the canvas tag or id of a tree node,
isleaf will return 1 if the node is a leaf node and 0
otherwise. In this case, if the node is a leaf node, we add its
subnodes by calling ListDirs to add the directories under the
selected directory.
The call to $tree draw causes the tree to be drawn on the
canvas. This call is not actually necessary, since the tree would be
drawn anyway, but it makes sure that the nodes (canvas items) are in
the right places before they become visible.
If the node is not a leaf, then it has subnodes and we want to remove
them from the tree. This is exactly what the prune tree
subcommand does. It removes any subnodes of the given node and the
corresponding canvas items, but otherwise leaves the node intact.
The other procedure, ToggleParent, is somewhat more
complicated. It makes the selected node the root of the tree, unless
it already is the root, in which case a new root and its subnodes is
inserted before the selected node.
The isroot tree subcommand is similar to isleaf used
above. It returns 1 if the given node is the root of the tree and 0
otherwise.
If the selected node is the root of the tree (but the directory is not
/), then we add a new subtree for the parent directory,
initially under the pseudo root node (tag = ""). We
then add the subdirectories of the new root node to the tree using
AddDir as before, except that when we come to the node that
was originally selected, instead of re-creating it, we move it to its
new position under the new root node using the movelink tree
subcommand. movelink works much like the UNIX mv
command when both arguments are directories. It unhooks the node
named by the first argument and inserts it as a child of the node
named by the second argument.
In the case where the selected node was not the root of the tree, we
simply use the tree root subcommand to make the given node
the new root of the tree. The canvas items for any nodes that are no
longer accessible from the root node are automatically deleted.
The figure below shows the tree after navigating up the directory hierarchy
a few levels.
Pruning Trees, Moving and Removing Nodes
In the previous example we used the prune and
movelink tree subcommands. The prune subcommand is
used to remove the subnodes from the given node. The movelink
command moves the given node to a new position in the tree. One other
command that we did not discuss yet is used to remove the named node
and all of its subnodes from the tree. The rmlink tree
subcommand is similar to the prune subcommand, except that
the given node is also removed from the tree. The example below adds
key bindings to the previous example so that Control-d removes the
selected node and its subtree.
bind $canvas "HideNode $canvas $tree"
# remove the selected node and its subnodes from the display
proc HideNode {canvas tree} {
set path [GetPath $canvas]
if {"$path" != "" && ![$tree isroot $path]} {
$tree rmlink $path
}
}
Setting a New Root Node
As we saw in the previous example, any tree node that is not already
the root of the tree can be made the root of the tree by using the
root subcommand. Any nodes that are not subnodes of the given
node will be deleted from the tree and the corresponding canvas items
deleted.
Displaying A Forest
When we talk about the root of the tree, we usually mean the
visible root. As mentioned earlier, there is actually a
pseudo root node that is not visible and has the empty tag
{}. Since you add the visible root by specifying this pseudo
root as the parent, it is also possible to display multiple trees in
one canvas by adding multiple visible root nodes under this node.
This kind of thing might be useful for displaying a class hierarchy
where there might not be a single root.
Traversing the Tree
There are a number of tree subcommands available for querying and
traversing the tree that is being displayed. Each of the following
subcommands takes as an argument the canvas tag or id of a tree node
and returns the tags of one or more tree nodes as a result:
- The subnodes command returns a Tcl list of the given
node's subnodes.
- The parent subcommand returns the name of the parent of
the given node. If the node has no parent (i.e.: is a root node), an
empty string is returned.
- The child subcommand returns the tag of the first child
node of the given node. If the node has no children, an empty string
is returned. The sibling subcommand can be used to get the
names of the other child nodes in sequence.
- The sibling subcommand returns the name of the next
sibling of the given node. If the node has no siblings, an empty
string is returned.
- The ancestors subcommand returns a Tcl list of the given
nodes ancestors, for example: {parent grandparent ...}.
Let's use the above commands to implement key bindings for our example
tree so that you can use the cursor (arrow) keys to navigate the tree.
# add bindings for the arrow keys
bind $canvas "SelectNext $canvas $tree %K"
bind $canvas "SelectNext $canvas $tree %K"
bind $canvas "SelectNext $canvas $tree %K"
bind $canvas "SelectNext $canvas $tree %K"
# select the current node's parent, child or sibling
# depending on the value of direction (Left, Right, Up or Down)
proc SelectNext {canvas tree direction} {
set id [$canvas select item]
set path [lindex [$canvas gettags $id] 0]
# for vertical trees its different...
if {[$tree cget -layout] == "vertical"} {
case $direction in {
Left {set direction Up}
Right {set direction Down}
Up {set direction Left}
Down {set direction Right}
}
}
case $direction in {
Left {
set node [$tree parent $path]
if {"$node" == ""} {
ToggleParent $canvas $tree
set node [$tree parent $path]
}
}
Right {
set node [$tree child $path]
if {"$node" == ""} {
ListDirs $canvas $tree $path
set node [$tree child $path]
$tree draw
}
}
Down {
set node [$tree sibling $path]
if {"$node" == ""} {
set node [$tree child [$tree parent $path]]
}
}
Up {
set next [$tree child [$tree parent $path]]
while {"$next" != ""} {
set node $next
set next [$tree sibling $next]
if {"$next" == "$path"} {
break;
}
}
}
default {return}
}
if {"$node" != ""} {
set next [$canvas find withtag $node:text]
$canvas select from $next 0
$canvas select to $next [string length [$canvas itemcget $next -text]]
}
}
The SelectNext procedure above takes as arguments the name of
the tree and canvas widgets and a direction, which is one of
the arrow key names Up, Down, Right or
Left. The action taken depends on the key was pressed, the
selected node and the layout of the tree. For example, if the tree
layout is horizontal, pressing Left will traverse the tree
selecting the current node's parent, adding it to the tree first if
necessary. Pressing Right will traverse the tree in the other
direction adding subtrees as needed. The Up and Down
arrow keys don't add any nodes, they only traverse the selected node's
siblings.
Reconfiguring Tree Nodes
When you make any changes to the canvas items that make up a tree
node, such as rearranging the layout, adding or removing items, the
tree widget needs to be told to recalculate the node's size. The
nodeconfigure subcommand does this and also allows you to
change the node's options. nodeconfig accepts the same
options as the addlink command:
- -border width
- Specifies the width of the nodes border. This determines the
distance between the individual nodes in the tree. The default is 0.
- -remove command
- Specifies a Tcl command to be evaluated when the node is removed
from the tree. This option is necessary when a node contains a window
such as a listbox or button, so that these will also be properly
deleted. The default action, when a node is to be removed, is to use
the canvas delete command to delete the canvas items with the
given tag or id.
The -remove option will be used in the next example, where we add
Tk windows to tree nodes.
Using Tk Windows in Tree Nodes
In the examples so far the trees have only displayed
directories. Normally you would also want to see a list of the files
in a selected directory. We could display files in the tree along with
the directories, say with a different bitmap, but that might clutter
up the tree too much, since there tend to be many more files than
directories. We could display a list of files in the selected
directory in a separate listbox. In fact, we could make the listbox a
part of a tree node and display it there when needed. Whether this is
a good idea or not probably depends on the application, but for the
sake of example, let's add bindings to the example directory tree
application so that clicking on a tree node with the middle mouse
button opens or closes a listbox under the node listing the files in
the directory. In order to be useful, the example also has to allow
resizing of the listbox and add code to get the name of a selected file.
# add a binding to tree node labels to open/close a listbox listing files in
# the selected directory
$canvas bind text <2> "focus %W; SelectNode $canvas; ShowFiles $canvas $tree"
# Display the file names in the selected directory in
# a scrolling list beneath the node
proc ShowFiles {canvas tree} {
set dir [GetPath $canvas]
set frame [UniqueName $canvas $dir]
# create the frame and list if not already there
if {![llength [$canvas find withtag $dir:list]]} {
MakeListFrame $canvas $tree $frame $dir
} else {
RemoveListFrame $canvas $tree $frame $dir
}
}
# generates a unique name for a widget from the given
# directory name by replacing the '.'s with '_'s
proc UniqueName {canvas dir} {
global dirtree
if {![catch {set path $dirtree($canvas.$dir)}]} {
return $path
}
if {[catch {incr dirtree(listboxcnt)}]} {
set dirtree(listboxcnt) 0
}
set dirtree($canvas.$dir) $canvas.list$dirtree(listboxcnt)
return $dirtree($canvas.$dir)
}
# make the list frame for displaying the files in dir
proc MakeListFrame {canvas tree frame dir} {
global dirtree
set bg [$canvas cget -bg]
# make the list frame and set up resizing
frame $frame -borderwidth 3 -cursor bottom_right_corner
scrollbar $frame.scroll -relief sunken -command "$frame.list yview" -width 10
listbox $frame.list -yscrollcommand "$frame.scroll set" -relief sunken \
-bg $bg -selectmode single
pack $frame.scroll -side right -fill y
pack $frame.list -side left -expand yes -fill both
# fill the list with the files in the selected dir
# (with the uninteresting files filtered out)
set n 0
foreach i [exec ls $dir] {
if {[file isfile $dir/$i]} {
incr n
$frame.list insert end $i
}
}
# insert the list frame in the canvas
$canvas create window 0 0 -tags "$dir $dir:list list" \
-window $frame -width 3c -height 2c
# arrange the items in the tree node and change the bitmap
# to show an "open" directory
LayoutNode $canvas $tree $dir
$canvas itemconfig $dir:bitmap -bitmap "@bitmaps/open_dir.xbm"
$tree nodeconfig $dir -remove "destroy $frame"
# add bindings for resizing the listbox in the canvas
SetFileListBindings $canvas $tree $frame $frame.list $frame.scroll $dir
}
# remove the list frame for displaying the files in dir
proc RemoveListFrame {canvas tree frame dir} {
global dirtree
$canvas delete $dir:list
destroy $frame
$canvas itemconfig $dir:bitmap -bitmap @bitmaps/dir.xbm
$tree nodeconfig $dir -remove "" -border 2
set dirtree(list) ""
set dirtree(dir) ""
}
# event proc called to resize the file listbox in the canvas
# while the user is dragging its corner
proc ResizeList {canvas tree frame list x y dir {when any}} {
global dirtree
case "$when" {
"first" {
set dirtree(tlx) $x
set dirtree(tly) $y
}
"last" {
$tree nodeconfig $dir
}
default {
set w [$canvas itemcget $dir:list -width]
set h [$canvas itemcget $dir:list -height]
set nw [expr $w+($x-$dirtree(tlx))]
set nh [expr $h+($y-$dirtree(tly))]
$canvas itemconfig $dir:list -width $nw -height $nh
set dirtree(tlx) $x
set dirtree(tly) $y
}
}
}
# set the bindings for a file listbox in the tree canvas
proc SetFileListBindings {canvas tree frame list scroll dir} {
global dirtree
bind $frame "ResizeList $canvas $tree $frame $list %x %y $dir first"
bind $frame "ResizeList $canvas $tree $frame $list %x %y $dir"
bind $frame "ResizeList $canvas $tree $frame $list %x %y $dir last"
bind $list <1> "set dirtree(list) %W; set dirtree(dir) $dir; [bind Listbox <1>]"
bind $list ChooseFile
# set/clear the resize cursor
bind $list "$frame config -cursor {}"
bind $list "$frame config -cursor bottom_right_corner"
bind $scroll "$frame config -cursor {}"
bind $scroll "$frame config -cursor bottom_right_corner"
}
# This proc is called when the user double-clicks on a filename
# in one of the listboxes listing the files in a directory.
proc ChooseFile {} {
set file [GetFilename]
if {"$file" == ""} {return}
# for test purposes, just print the file name here
puts "got $file"
}
# Return the path name of the file currently selected in the
# current node's listbox, or the empty string if none are selected.
proc GetFilename {} {
global dirtree
set dir $dirtree(dir)
set list $dirtree(list)
if {"$dir" == "" || "$list" == ""} {return ""}
set sel [$list curselection]
if {![llength $sel]} {return ""}
return $dir/[$list get [lindex $sel 0]]
}
# layout the components of the given node depending on whether
# the tree is vertical or horizontal
# (Also take the file listbox into consideration if it is open)
proc LayoutNode {canvas tree dir} {
set text $dir:text
set bitmap $dir:bitmap
set list [$canvas find withtag $dir:list]
if {[$tree cget -layout] == "horizontal"} {
scan [$canvas bbox $text] "%d %d %d %d" x1 y1 x2 y2
$canvas itemconfig $bitmap -anchor se
$canvas coords $bitmap $x1 $y2
if {"$list" != ""} {
scan [$canvas bbox $text $bitmap] "%d %d %d %d" x1 y1 x2 y2
$canvas itemconfig $list -anchor nw
$canvas coords $list $x1 $y2
}
} else {
scan [$canvas bbox $bitmap] "%d %d %d %d" x1 y1 x2 y2
$canvas itemconfig $text -anchor n
$canvas coords $text [expr "$x1+($x2-$x1)/2"] $y2
if {"$list" != ""} {
scan [$canvas bbox $text $bitmap] "%d %d %d %d" x1 y1 x2 y2
$canvas itemconfig $list -anchor n
$canvas coords $list [expr "$x1+($x2-$x1)/2"] $y2
}
}
}
The figure below shows the example application now after some file lists have been
opened and resized.
As usual when working with the tree widget, most of the operations
have more to do with the canvas widget than with the tree. The tree
widget only arranges the nodes in the canvas.
The ShowFiles procedure in the previous example is called to
open or close the file list for the selected node in the tree. If the
file list is present in the tree (the canvas tag $dir:list is
found), it is removed and deleted, otherwise a new frame with a unique
name is created as a child of the canvas widget. The listbox and
scrollbar will be packed into this frame.
The MakeListFrame procedure takes care of filling out the
contents of the new frame in the tree. It creates the listbox and
scrollbar and packs them in the frame and then fills the listbox with
the list of files in the selected directory. The LayoutNode
procedure from earlier examples has been extended here to handle the
layout when a listbox frame is part of the node. We also add some
bindings to the listbox frames here so that you can resize them by
dragging on the corner of the frame with mouse button 1.
The important thing to remember when working with windows in tree
nodes or as canvas items in general is that, since they are not
managed by a geometry manager, such as the packer, you have
to set and get the window size with canvas commands and not with the
-width or -height widget options. We set the default
size of the listbox frame when we create the canvas window item:
$canvas create window 0 0 -tags "$dir $dir:list list" \
-window $frame -width 3c -height 2c
And later, when we want to resize the listbox frame interactively, we use
the canvas itemconfig subcommand:
set w [$canvas itemcget $dir:list -width]
set h [$canvas itemcget $dir:list -height]
...
$canvas itemconfig $dir:list -width $nw -height $nh
It is important that the canvas knows the size of the window, so that
the tree can get the correct information from the canvas about the
size of the the tree nodes.
An [incr Tcl] Interface to the Tree Widget
The example directory tree application developed in the previous
section only made use of standard Tcl, Tk and the tree extension. In
practice, I almost always use a wish with at least the
[incr Tcl], TclX and BLT extensions also
added. In particular, I prefer to organize Tcl code in an object
oriented way using [incr Tcl] classes. The [incr Tcl] extension, also
called Itcl, adds classes and inheritance to Tcl and also
includes features that make it easy to create mega-widgets or
combinations of widgets that behave like the standard Tk widgets.
The tree widget release also contains a library of Itcl mega-widgets,
including a wrapper for the basic tree widget. In order to
use the Itcl version of the tree widget, you use the capitalized
widget name Tree instead of tree. The Itcl tree
widget accepts all of the standard tree options and subcommands and
passes them on to the underlying tree widget. In addition, the Itcl
widget takes care of creating the canvas, tree and scrollbars, defines
a standard layout for tree nodes, handles node selection and offers
methods for common operations on trees. Below is an example of a
simple tree application using the Itcl tree class.
# This is an example of a simple "application" class, i.e.:
# this program does nothing but create an instance of this class.
itcl_class Simple {
inherit TopLevelWidget
# constructor: create a toplevel window for the demo
constructor {config} {
TopLevelWidget::constructor
wm title $this {Simple [incr Tcl] Tree Demo}
wm minsize $this 10 10
# create an instance of the Itcl "Tree" widget
# (based on the C++ "tree" widget)
pack [Tree $this.tree] -fill x -expand 1
# add a row of buttons at botton
pack [ButtonFrame $this.b -ok_cmd exit] -side bottom -fill x -expand 1
# add some nodes to the tree
add_nodes {} root
add_nodes root node1 node2 node3 node4
add_nodes node1 node1.1 node1.2 node1.3
add_nodes node2 node2.1 node2.2 node2.3
add_nodes node3 node3.1 node3.2
}
# Add one or more subnodes to the given parent node.
# In this case, the label is just the same as the node's tag
method add_nodes {parent args} {
foreach node $args {
$this.tree add_node $parent $node \
-bitmap @$bitmap \
-label $node
}
}
# -- public variables (also program options) --
public bitmap "../bitmaps/node.xbm"
}
# Start the demo:
# Note that "start" is a "member" proc in the TopLevelWidget class.
# It creates an instance of the above class and handles options and
# error checking.
Simple :: start
The figure below shows the window created by the simple application above:
In the example above, we create a class named Simple that
inherits from the TopLevelWidget class, which is part of the
Itcl library included in the tree distribution. While, neither of
these are needed in order to use the Itcl tree widget, they
demonstrate some of the things you can do with Itcl. The
TopLevelWidget class creates a new toplevel window
with the same name as the class object and also defines a number of
useful methods that can be used by the derived classes. One of these
methods (actually a class procedure, since it does not require an
instance of the class) is used at the end of the example to start the
application:
Simple :: start
Calls the start procedure in the TopLevelWidget
class to withdraw the default Tk main window "." (the
Simple class has its own toplevel window), create an instance
of the Simple class and pass it any command line arguments as
class options. This is eqivalent to the following, but with some
extra error reporting code added to give the user a list of valid
options if an unknown option is found:
wm withdraw .
eval "Simple .simple $argv"
tkwait window .simple
The Itcl tree is then created and packed in the normal way with this
command:
pack [Tree $this.tree] -fill x -expand 1
The actual window created here is a frame called $this.tree
containing a canvas with two scrollbars and the tree widget. Note
that $this is the name of the instance of the Simple
class that we are working with and also the name of the toplevel
window in this example.
Another Itcl class, ButtonFrame is used to add a frame at the
bottom of the window with an OK and a Cancel button,
used to end the application.
To be completed...
A Simple Drag&Drop Tree Application
To be completed...